package client.net;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;
import javax.media.ConfigureCompleteEvent;
import javax.media.ControllerEvent;
import javax.media.ControllerListener;
import javax.media.DataSink;
import javax.media.Format;
import javax.media.Manager;
import javax.media.MediaLocator;
import javax.media.Processor;
import javax.media.RealizeCompleteEvent;
import javax.media.control.BufferControl;
import javax.media.control.TrackControl;
import javax.media.protocol.DataSource;
import javax.media.protocol.FileTypeDescriptor;
import javax.media.rtp.RTPManager;
import javax.media.rtp.ReceiveStream;
import javax.media.rtp.ReceiveStreamListener;
import javax.media.rtp.event.ByeEvent;
import javax.media.rtp.event.NewReceiveStreamEvent;
import javax.media.rtp.event.ReceiveStreamEvent;

import client.audio.FileAudio;
import client.container.BtPortContainer;
import client.container.FileCompletedContainer;


/**
 * @author primianoc
 *
 */
public class StreamManager implements Runnable, ReceiveStreamListener, ControllerListener
{
	private RTPManager _rtpManager = null;
	private RTPL2capAdapterReceiver _rtpL2capAdapterReceiver = null;
	private File _fileTarget = null;
	private String _btPort = null;
	private boolean _skipHeader = false;

	/**
	 *  Formato dei file creati dai processor.
	 */
	private Format _formatSelected = null;

	private Vector<Processor> _processorVector = new Vector<Processor>();
	private Vector<File> _fileTempVector = new Vector<File>();

	private String _pathMediaFiles = null;
	private String _sessionName = null;
	private String _fileExtension = null;
	private int _indexTempFile = -1;
	private int _indexSubfixFile = -1;

	private Object _synchTempFile = new Object();
	private boolean _startTimeChanged = false;
	private int _bytesHeader = 24;
	private boolean _fireNewStream = true;

	private int  _changeCoding = 0;
	private Object _syncChangeCoding = new Object();
	private int _startTime = 0;
	private boolean _streamContainerActive = true;
	private Object _syncProcessor = new Object();

	/**
	 * OutputStream costruito a partire dal file targe.
	 */
	private OutputStream _outputStream = null;

	private SessionManager _sessionManager = null;
	private boolean _closing = false;
	private boolean _partialCompleted = false;
	private boolean _resetFileTarget = false;
	private boolean _streamError = false;
	private boolean _completed = false;
	private int totalBytesWritten = 0;
	private Timer _timer = null;

	/**
	 * @return the _completed
	 */
	public boolean isCompleted()
	{
		return _completed;
	}

	/**
	 * @param resetFileTarget the _resetFileTarget to set
	 */
	public void resetFileTarget()
	{
		_resetFileTarget = true;
	}

	/**
	 * @return the _partialCompleted
	 */
	public boolean isPartialCompleted()
	{
		return _partialCompleted;
	}

	public StreamManager(SessionManager sessionManager, String btPort,  
			String pathMediaFiles, String sessionName,  String fileExtension, 
			int indexTempFile, File fileTarget, Format formatSelected, int startTime)
	{
		_sessionManager = sessionManager;
		_btPort = btPort;
		_fileTarget = fileTarget;
		_pathMediaFiles = pathMediaFiles;
		_sessionName =sessionName;
		_fileExtension = fileExtension;
		_indexTempFile =indexTempFile;
		_formatSelected = formatSelected;
		_startTime = startTime;
		init();
	}

	/* (non-Javadoc)
	 * @see java.lang.Runnable#run()
	 */
	@Override
	public void run()
	{

		try
		{
			_outputStream = new FileOutputStream(_fileTarget, true);
		}
		catch (FileNotFoundException e)
		{
			e.printStackTrace();
			return;
		}

		if(_resetFileTarget == true)
		{
			_resetFileTarget = false;

			try
			{
				startTimeChanged();
				_startTime = 0;
			} 
			catch (IOException e)
			{
				e.printStackTrace();
			}
		}

		startTimerInactivity();

		while(_streamContainerActive  == true)
		{

			if(_fileTempVector.isEmpty() == true)
			{
				waitFileTemp();
			}



			System.out.println("DEBUG StreamManager: Il vettore _fileTempVector contiene " +
					_fileTempVector.size() + " elementi.");

			File fileTemp = _fileTempVector.firstElement();

			System.out.println("DEBUG StreamManager: " +
					"_fileTempVector.firstElement().getName() " + fileTemp.getName());


			InputStream inputStream = null;
			boolean copyEnable = true;
			boolean verifyFileTemp = false;

			if(_skipHeader == true)
			{
				totalBytesWritten = _bytesHeader;
				_skipHeader = false;
			}

			while(copyEnable == true)
			{
				/*
				 *  Creo un InputStream a partire dal fileTemp.
				 */
				try
				{
					inputStream = new FileInputStream(fileTemp);
				} 
				catch (FileNotFoundException e)
				{
					e.printStackTrace();
					return;
				}

				//				System.out.println("DEBUG StreamManager: eseguito new FileInputStream(fileTemp)");


				try
				{
					/**
					 *  Se l'input stream ha piu' byte di quelli gia scritti 
					 *  allora copio sul file destinazione i byte in piu'
					 */
					if(inputStream.available() > totalBytesWritten)
					{
						// Mando avanti lo stream per evitare di copiare nuovamente 
						// i byte che ho gia scritto nel file destinazione.

						inputStream.skip(totalBytesWritten);


						// In ogni ciclo di copia considero 80 KByte circa
						byte[] buf = new byte[81920];

						// Leggo len byte dall'input stream e li salvo nell'array 
						// di byte, poi li scrivo nell'outputStream.
						int len = 0; 

						do 
						{
							len = inputStream.read(buf);

							if(len > 0)
							{
								_outputStream.write(buf, 0, len);
								totalBytesWritten = totalBytesWritten + len;
							}

						} while (len > 0 && _startTimeChanged == false);

						_outputStream.flush();

						if(verifyFileTemp == true)
						{
							verifyFileTemp = false;
						}

						/**
						 *  Se è stato modificato lo startTime bisogna 
						 *  eliminare l'attuale file target e crearne uno
						 *  nuovo. 
						 */
						if(_startTimeChanged == true)
						{
							startTimeChanged();
							totalBytesWritten = totalBytesWritten + 10000;
							System.out.println("DEBUG StreamManager: eseguito " +
							"cambio start Time e azzeramento file target.");
						}
						else
						{
							if(_fireNewStream == true)
							{
								/*
								 * Costruisco un player jmf per ottenere la lunghezza del file target in secondi.	
								 */
								int fileLength = -1;

								try
								{
									fileLength = FileAudio.getInstance().getDurationSeconds(_fileTarget);
								}
								catch (Exception e)
								{
									e.printStackTrace();
								}
								System.out.println("DEBUG StreamManager: fileLength " + fileLength);

								if(fileLength > 4)
								{_fireNewStream = false;

								/**
								 *  Scateno l'evento NewStream.
								 */
								_sessionManager.fireNewStream();
								}
							}

							_sessionManager.fireNewDataReceived();
						}
					}
					else
					{

						/*
						 * Non ci sono nuovi byte disponibili da copiare. Se è arrivato
						 * un ByeEvent di cambio codifica bisogna passare al file temp
						 * successivo.
						 */
						if(_changeCoding > 0)
						{
							if(verifyFileTemp = false)
							{
								verifyFileTemp = true;	
							}
							else
							{
								if(inputStream.available() == totalBytesWritten)
								{
									synchronized(_syncChangeCoding)
									{
										System.out.println("DEBUG StreamManager: decremento change coding" +
												" dal valore " + _changeCoding + " al valore " +
												(_changeCoding -1));

										_changeCoding = _changeCoding -1;
									}

									copyEnable = false;

									/**
									 * Siccome c'è un cambio di codifica in corso
									 * nel prossimo ciclo deve essere eliminato 
									 * l'header file dal file temp prima di copiarne 
									 * il contenuto nel file target.
									 */
									_skipHeader = true;

									synchronized(_synchTempFile)
									{
										_fileTempVector.removeElement(fileTemp);
										fileTemp.delete();
									}
								}
								else
								{
									System.err.println("DEBUG StreamManager: " +
											"attenzione inputStream.available() " +
											inputStream.available() + " totalBytesWritten " +
											totalBytesWritten + " dovrebbero essere uguali.");

									verifyFileTemp = false;
								}
							}
						}
						else
						{
							/**
							 * Se _closing è true allora ho ricevuto
							 * un ByeEvent 
							 */
							if(_closing  == true )
							{
								if(verifyFileTemp = false)
								{
									verifyFileTemp = true;	
								}
								else
								{
									if(inputStream.available() <= totalBytesWritten)
									{
										copyEnable = false;

										synchronized(_synchTempFile)
										{
											_fileTempVector.removeElement(fileTemp);
											fileTemp.delete();
										}

										if(_fileTempVector.isEmpty() == true)
										{
											_streamContainerActive = false;
										}
										else
										{
											System.err.println("DEBUG StreamManager: " +
													" si è verificata una condizione anomala." +
											" Non dovrebbero esserci ulteriori file temporanei.");
											verifyFileTemp = false;
										}
									}
									else
									{
										System.err.println("DEBUG StreamManager: " +
												"attenzione inputStream.available() " +
												inputStream.available() + " totalBytesWritten " +
												totalBytesWritten );
										verifyFileTemp = false;
									}
								}

							}
						}

					}

					inputStream.close();

					/*
					 * Importante: bisogna inserire uno sleep 
					 * maggiore o uguale a 1 secondo, altrimenti
					 * i thread che gestiscono le connessioni 
					 * bluetooth non vengono schedulati.
					 */

					Thread.sleep(1000);
				} 
				catch (Exception e)
				{
					e.printStackTrace();

				}

			}// fine while(_copyEnable)
		}// fine while(_streamContainerActive  == true)

		// Chiudo l'outputStream
		try
		{
			_outputStream.close();
		} 
		catch (IOException e)
		{
			e.printStackTrace();
		}

		System.out.println("DEBUG StreamManager: thread " + 
				Thread.currentThread().getName()+  
				"eseguo la close" +
				" e termino. Nome stream: " + _sessionName);
		close();

		if(_streamError == true)
		{
			_sessionManager.fireStreamError();
		}
	}

	private void startTimerInactivity()
	{		
		if(_timer == null)
		{
			_timer = new Timer();

			/**
			 * Imposto il task che controlla se lo stream
			 * è attivo.
			 */
			_timer.schedule(new InactivityTask(),0,10000);
		}
	}

	public synchronized void close()
	{
		_closing = true;

		if(_rtpL2capAdapterReceiver != null)
		{
			_timer.cancel();

			if(_rtpL2capAdapterReceiver.isConnectionsOpened() == true)
			{
				_rtpManager.removeTargets("DISACTIVE");
				System.out.println("DEBUG StreamManager - metodo close(): eseguito removeTargets");

			}

			_rtpManager.dispose();
			System.out.println("DEBUG StreamManager - metodo close(): eseguito dispose()");

			_rtpManager = null;
			_rtpL2capAdapterReceiver = null;

			/*
			 * Restituisco la porta al container.
			 */
			BtPortContainer.getInstance().insertBtPsm(_btPort);
		}

	}

	public boolean sendNewStartTime(int newStartTime)
	{
		String newStartTimeString = String.valueOf(newStartTime);

		int byteSent = -1;

		if(_rtpL2capAdapterReceiver.getDataConnectionManager() != null)
		{
			byteSent = _rtpL2capAdapterReceiver.getDataConnectionManager().send(newStartTimeString.getBytes(), false);
		}

		if(byteSent > -1)
		{
			_startTimeChanged = true;
			_startTime = newStartTime;
			return true;
		}
		else
		{
			return false;
		}
	}

	private void startTimeChanged() throws IOException
	{
		_outputStream.close();
		_outputStream = null;

		/*
		 * Azzero il file target e scrivo nel nuovo file 
		 * l'header di quello vecchio.
		 */
		InputStream inputStreamTemp = new FileInputStream(_fileTarget);

		byte[] bufferTemp = new byte[_bytesHeader];
		int length = inputStreamTemp.read(bufferTemp);

		_fileTarget.delete();
		_fileTarget.createNewFile();
		_outputStream = new FileOutputStream(_fileTarget, true);
		_outputStream.write(bufferTemp, 0, length);

		_startTimeChanged = false;

		// Nel prossimo ciclo verrà generato l'evento
		// invocando il metodo fireNewStream
		_fireNewStream = true;
	}

	private void waitFileTemp()
	{
		synchronized(_synchTempFile)
		{
			while(_fileTempVector.isEmpty() == true)
			{
				try
				{
					System.out.println("DEBUG StreamManager: _fileTempVector.isEmpty() == " +
							_fileTempVector.isEmpty() + " vado in wait.");
					_synchTempFile.wait();
					System.out.println("DEBUG StreamManager: _fileTempVector.isEmpty() == " +
							_fileTempVector.isEmpty() + " ricevuta notify. ");
				} 
				catch (InterruptedException e)
				{
					e.printStackTrace();
				}
			}
		}
	}

	@Override
	public void update(ReceiveStreamEvent receiveStreamEvent)
	{
		if (receiveStreamEvent instanceof NewReceiveStreamEvent) 
		{
			/**
			 *  Estraggo il ReceiveStream dall'evento receiveStreamEvent.
			 */
			ReceiveStream receiveStream = receiveStreamEvent.getReceiveStream();

			/** 
			 * Estraggo il dataSource dal receiveStream
			 */
			DataSource dataSource = receiveStream.getDataSource();

			Processor processor = null;

			try
			{
				processor = Manager.createProcessor(dataSource);
				processor.addControllerListener(this);
			}
			catch (Exception e)
			{
				e.printStackTrace();
				return;
			}

			synchronized(_syncProcessor)
			{
				/**
				 *  Inserisco il processor nel Vector.
				 */
				_processorVector.add(processor);

				if(_processorVector.firstElement().equals(processor))
				{
					/**
					 * Il processor appena inserito è il primo
					 * elemento del vettore. Posso eseguire il
					 * metodo configure.
					 */
					_processorVector.firstElement().configure();
				}
			}

		}
		else
		{
			if (receiveStreamEvent instanceof ByeEvent) 
			{
				ByeEvent byeEvent = (ByeEvent)receiveStreamEvent;

				String reason = byeEvent.getReason();

				if(reason.contains("COMPLETED"))
				{
					if(_startTime == 0)
					{
						FileCompletedContainer.getInstance().setStreamCompleted(_sessionName);
						_completed = true;
					}
					else
					{
						/**
						 * Lo stream è terminato ma non è stato salvato 
						 * completamente. Manca la parte iniziale.
						 */
						_partialCompleted  = true;
					}

					_closing = true;
				}
				else
				{
					if(reason.contains("FAILOVER"))
					{
						/**
						 * Usurpo in corso. 
						 */
						_closing = true;

					}
					else 
					{
						if(reason.contains("CHANGECODING"))
						{
							synchronized(_syncChangeCoding )
							{	
								_changeCoding = _changeCoding +1;
							}
						}
						else
						{
							if(reason.contains("ERROR") || reason.contains("STOPPED"))
							{
								/**
								 * C'è stato un errore durante il trasferimento
								 * oppure il gateway lo ha interrotto volontariamente.
								 */
								_closing = true;
								_streamError = true;
							}
						} 
					}

				}
			}
		}
	}

	@Override
	public void controllerUpdate(ControllerEvent controllerEvent)
	{
		//questo if può essere eliminato perche' il controller può essere solo un Processor
		if(controllerEvent.getSourceController() instanceof Processor)
		{
			Processor processor = (Processor)controllerEvent.getSourceController();

			if(controllerEvent instanceof ConfigureCompleteEvent)
			{
				/**
				 * Imposto il ContentDescriptor del Processor come BASIC_AUDIO
				 */
				processor.setContentDescriptor(new FileTypeDescriptor(FileTypeDescriptor.BASIC_AUDIO));

				TrackControl [] tracks = processor.getTrackControls();

				tracks[0].setFormat(_formatSelected);

				processor.realize();			
			}
			else
			{
				if(controllerEvent instanceof RealizeCompleteEvent)
				{
					/*
					 * Il processor è pronto e file temp può essere inserito
					 * nel vettore dei file da copiare sul target.
					 */

					// Creazione del file in cui verrà scritto lo stream ricevuto

					_indexSubfixFile = _indexSubfixFile + 1;

					File file = new File(_pathMediaFiles + _sessionName  + ".temp" +
							_indexTempFile + "." + _indexSubfixFile + _fileExtension);

					// Se esiste un file temporaneo con lo stesso nome allora lo cancello
					if(file.length() > 0)
					{
						file.delete();

						try
						{
							file.createNewFile();
						}
						catch (IOException e1)
						{
							e1.printStackTrace();
							return;
						}
					}

					// Oggetto usato per la creazione del DataSink
					MediaLocator mediaLocator = null;

					try
					{
						mediaLocator = new MediaLocator(file.toURI().toURL());
					}
					catch (MalformedURLException e1)
					{
						e1.printStackTrace();
						return;
					}

					// Estrazione del DataSource creato dal processor
					DataSource dataOutput = processor.getDataOutput();

					DataSink dataSink = null;

					try
					{
						dataSink = Manager.createDataSink(dataOutput, mediaLocator);

						// Opens a connection to the destination described by the 
						// output MediaLocator. This method establishes a channel 
						// between this DataSink and its destination.
						dataSink.open();

						// Initiates data transfer. You must call open before calling
						// the start method.
						dataSink.start();

						// Metodo ereditato dalla classe Player. 
						processor.start();

					}
					catch (Exception e)
					{
						e.printStackTrace();
						return;
					}

					/*
					 * Il file temp è pronto. Lo aggiungo al vettore di File ed
					 * eseguo il metodo notify sull'oggetto _synchTempFile su
					 * cui ha eseguito il metodo wait il thread this se aveva 
					 * trovato il vettore vuoto. 
					 */
					synchronized(_synchTempFile)
					{
						_fileTempVector.add(file);

						_synchTempFile.notify();
					}


					/*
					 * Rimuovo il processor dal vettore e, se è presente un altro processor, ne invoco 
					 * il metodo configure.
					 */
					synchronized(_syncProcessor)
					{
						_processorVector.removeElement(processor);

						if(_processorVector.isEmpty() == false)
						{
							/*
							 * Esiste un altro processor nel vettore, quindi
							 * posso eseguire il metodo configure.
							 */
							_processorVector.firstElement().configure();
						}
					}

				} //fine if(controllerEvent instanceof RealizeCompleteEvent)
			}
		}// fine if(controllerEvent.getSourceController() instanceof Processor)


	}

	private void init()
	{
		_rtpManager = RTPManager.newInstance();

		int bufferLength = 50;

		/**
		 *  Imposto il valore del buffer.
		 */
		BufferControl bc = (BufferControl)_rtpManager.getControl("javax.media.control.BufferControl");

		if (bc != null)
		{
			bc.setBufferLength(bufferLength);
		}

		/**
		 *  Intercetto gli eventi di tipo ReceiveStreamEvent generati dall'RTPManager.
		 */
		_rtpManager.addReceiveStreamListener(this);

		/**
		 *  Istanzio un RTPL2capAdapterReceiver che permette all'RTPManager di utilizzare
		 *  il protocollo L2CAP come trasporto.
		 */
		_rtpL2capAdapterReceiver = new RTPL2capAdapterReceiver(_btPort);

		/** 
		 * Assegno l'adapter all'RTPManager.
		 */
		_rtpManager.initialize(_rtpL2capAdapterReceiver);
	}

	class InactivityTask extends TimerTask
	{
		private int _bytewritten = -1;
		private boolean _inactivity = false;

		public void run()
		{	
			if(_bytewritten != totalBytesWritten)
			{
				_bytewritten = totalBytesWritten;
				_inactivity = false;
			}
			else
			{
				if(_inactivity == false)
				{
					_inactivity = true;
				}
				else
				{
					_closing = true;
					_streamError = true;
					this.cancel();
				}
			}

		}
	}

}
